Skip to content

support for uv mode#51

Open
pld wants to merge 1 commit intomainfrom
uv-support
Open

support for uv mode#51
pld wants to merge 1 commit intomainfrom
uv-support

Conversation

@pld
Copy link
Copy Markdown
Member

@pld pld commented Apr 25, 2026

Summary

Adds an opt-in django_use_uv install mode alongside the existing django_use_regular_old_pip / django_use_pipenv / django_use_poetry modes. The mode reads pyproject.toml + uv.lock from the project checkout and runs uv sync --frozen --no-dev into the same venv path the rest of the role expects (django_venv_path), so uwsgi systemd units, the wsgi config, etc. don't change.

Off by default — no behavior change for existing consumers.

Why

Driven by onaio/tally-ho#561, which migrated tally-ho off requirements/*.pip to a pyproject.toml + uv.lock workflow. Without this mode, the next deploy via onaio/ansible-tally-ho would fail at the pip install -r requirements/dev.pip step (the file no longer exists upstream). The downstream wiring lives in onaio/ansible-tally-ho#25.

What changed

  • defaults/main.yml — new django_use_uv, django_uv_version, django_uv_project_dir, django_uv_sync_args.
  • tasks/python.yml — installs the uv binary to /usr/local/bin/uv via the official astral.sh installer when the mode is enabled. Idempotent via creates:.
  • tasks/install.yml — runs uv sync with UV_PROJECT_ENVIRONMENT={{ django_venv_path }}, UV_LINK_MODE=copy, UV_COMPILE_BYTECODE=1. Placed before the existing django_pip_packages task so extras (uwsgi, celery, …) install into the uv-created venv unchanged.
  • README.md — documents the new mode and tunables.
  • molecule/uv/ — new molecule scenario with a tiny self-contained fixture project (tests/fixtures/sample-uv-project/) seeded as a local git repo by prepare.yml. testinfra checks: uv binary present, venv populated, django-admin exists, and a django_pip_packages extra (click) lands in the same venv — verifying the cohabitation that tally-ho relies on for uwsgi.

Test plan

  • ansible-lint clean (verified locally)
  • yamllint clean (verified locally)
  • CI molecule test --all exercises the new uv scenario on ubuntu2204
  • Reviewer eyeballs the new install path for shell injection, idempotency, ownership semantics

@FrankApiyo
Copy link
Copy Markdown
Member

Idempotency: creates: silently skips version upgrades

In tasks/python.yml, the Install uv (Astral) task uses creates: /usr/local/bin/uv. Once uv is installed, the task never re-runs — even when an operator bumps django_uv_version (e.g. 0.6.100.7.0). The next playbook run is a silent no-op and the upgrade never happens.

The comment in defaults/main.yml says "Pinning makes the install task's creates: idempotency check meaningful" — but pinning actually makes the broken-upgrade case more likely, since the operators most likely to pin are the ones who later want to bump.

Suggested fix: probe the installed version first, then install only when it doesn't match.

- name: Check installed uv version
  ansible.builtin.command: /usr/local/bin/uv --version
  register: _uv_installed
  failed_when: false
  changed_when: false
  become: true
  become_user: root
  when: django_use_uv

- name: Install uv (Astral)
  vars:
    _uv_installer_url: >-
      {{
        'https://astral.sh/uv/install.sh'
        if django_uv_version == 'latest'
        else 'https://astral.sh/uv/' ~ django_uv_version ~ '/install.sh'
      }}
  ansible.builtin.shell: |
    set -eo pipefail
    curl -LsSf {{ _uv_installer_url }} | env UV_INSTALL_DIR=/usr/local/bin sh
  args:
    executable: /bin/bash
  become: true
  become_user: root
  when:
    - django_use_uv
    - _uv_installed.rc != 0
      or django_uv_version == 'latest'
      or django_uv_version not in (_uv_installed.stdout | default(''))

The when: covers three cases: not-yet-installed (rc != 0), latest mode (re-run every time since you can't compare against a moving target), and pinned version mismatch.

One sharp edge worth noting: "0.6.1" in "uv 0.6.10 ..." is True, so a strict-semver pin like 0.6.1 would skip an upgrade to 0.6.10. For the typical full-semver pin (0.6.10) this isn't an issue, but if you want defense in depth, anchor with regex_search instead of in.

@FrankApiyo
Copy link
Copy Markdown
Member

Idempotency: Bootstrap pip into the uv-managed venv always reports changed

In tasks/install.yml, the new bootstrap-pip task hardcodes changed_when: true:

- name: Bootstrap pip into the uv-managed venv
  ansible.builtin.command:
    cmd: >-
      uv pip install
      --python {{ django_venv_path }}/bin/python
      pip
  environment:
    UV_LINK_MODE: copy
  become: true
  become_user: "{{ django_system_user }}"
  changed_when: true
  when:
    - django_use_uv| bool

Every playbook run reports this task as changed even when pip is already present. Two effects:

  1. Misleading PLAY RECAP output — operators stop noticing real changes when every deploy reports changed=N for no reason.
  2. Wasted work — uv pip install pip re-runs on every converge.

Suggested fix: add a creates: guard pointing to the file the command produces, and drop the hardcoded changed_when.

- name: Bootstrap pip into the uv-managed venv
  ansible.builtin.command:
    cmd: >-
      uv pip install
      --python {{ django_venv_path }}/bin/python
      pip
    creates: "{{ django_venv_path }}/bin/pip"
  environment:
    UV_LINK_MODE: copy
  become: true
  become_user: "{{ django_system_user }}"
  when:
    - django_use_uv| bool

Note this is not the same footgun as the creates: issue I flagged on the uv installer task — pip's version isn't a tunable in this role, so there's no upgrade path to silently skip. If an operator wants a specific pip version, they'd put it in django_dependency_pip_packages and ansible.builtin.pip handles it.

Low severity / polish — the same always-changed pattern exists for the poetry and pipenv tasks in this file, so the author was being consistent. Just flagging it as a chance to do it right for the new mode.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants